<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>流式TTS测试 - 火山引擎</title>
    <script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            padding: 20px;
        }

        .container {
            max-width: 900px;
            margin: 0 auto;
            background: white;
            border-radius: 20px;
            box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
            overflow: hidden;
        }

        .header {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            padding: 30px;
            text-align: center;
        }

        .header h1 {
            font-size: 2em;
            margin-bottom: 10px;
        }

        .header p {
            opacity: 0.9;
        }

        .content {
            padding: 30px;
        }

        .section {
            margin-bottom: 30px;
        }

        .section-title {
            font-size: 1.2em;
            font-weight: 600;
            color: #333;
            margin-bottom: 15px;
            display: flex;
            align-items: center;
        }

        .section-title::before {
            content: "";
            display: inline-block;
            width: 4px;
            height: 20px;
            background: #667eea;
            margin-right: 10px;
            border-radius: 2px;
        }

        .input-group {
            margin-bottom: 15px;
        }

        label {
            display: block;
            margin-bottom: 8px;
            color: #555;
            font-weight: 500;
        }

        textarea {
            width: 100%;
            padding: 12px;
            border: 2px solid #e0e0e0;
            border-radius: 8px;
            font-size: 14px;
            resize: vertical;
            transition: border-color 0.3s;
            font-family: inherit;
        }

        textarea:focus {
            outline: none;
            border-color: #667eea;
        }

        .params-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
            gap: 15px;
        }

        select, input[type="number"] {
            width: 100%;
            padding: 10px;
            border: 2px solid #e0e0e0;
            border-radius: 8px;
            font-size: 14px;
            transition: border-color 0.3s;
        }

        select:focus, input[type="number"]:focus {
            outline: none;
            border-color: #667eea;
        }

        .button-group {
            display: flex;
            gap: 15px;
            margin-top: 20px;
        }

        button {
            flex: 1;
            padding: 15px;
            border: none;
            border-radius: 8px;
            font-size: 16px;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.3s;
        }

        button:disabled {
            opacity: 0.5;
            cursor: not-allowed;
        }

        .btn-primary {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
        }

        .btn-primary:hover:not(:disabled) {
            transform: translateY(-2px);
            box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
        }

        .btn-secondary {
            background: #f5f5f5;
            color: #333;
        }

        .btn-secondary:hover:not(:disabled) {
            background: #e0e0e0;
        }

        .btn-danger {
            background: #ff4757;
            color: white;
        }

        .btn-danger:hover:not(:disabled) {
            background: #ee5a6f;
        }

        .status {
            padding: 15px;
            border-radius: 8px;
            margin-bottom: 20px;
            font-size: 14px;
        }

        .status-info {
            background: #e3f2fd;
            color: #1976d2;
            border-left: 4px solid #1976d2;
        }

        .status-success {
            background: #e8f5e9;
            color: #388e3c;
            border-left: 4px solid #388e3c;
        }

        .status-error {
            background: #ffebee;
            color: #d32f2f;
            border-left: 4px solid #d32f2f;
        }

        .status-warning {
            background: #fff3e0;
            color: #f57c00;
            border-left: 4px solid #f57c00;
        }

        .progress-bar {
            width: 100%;
            height: 6px;
            background: #e0e0e0;
            border-radius: 3px;
            overflow: hidden;
            margin-top: 10px;
        }

        .progress-fill {
            height: 100%;
            background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
            width: 0%;
            transition: width 0.3s;
        }

        .log-container {
            background: #f5f5f5;
            border-radius: 8px;
            padding: 15px;
            max-height: 200px;
            overflow-y: auto;
            font-family: 'Courier New', monospace;
            font-size: 12px;
        }

        .log-entry {
            padding: 5px 0;
            border-bottom: 1px solid #e0e0e0;
        }

        .log-entry:last-child {
            border-bottom: none;
        }

        .log-time {
            color: #999;
            margin-right: 10px;
        }

        audio {
            width: 100%;
            margin-top: 15px;
        }

        .stats {
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            gap: 15px;
            margin-top: 15px;
        }

        .stat-card {
            background: #f5f5f5;
            padding: 15px;
            border-radius: 8px;
            text-align: center;
        }

        .stat-value {
            font-size: 24px;
            font-weight: 700;
            color: #667eea;
            margin-bottom: 5px;
        }

        .stat-label {
            font-size: 12px;
            color: #666;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>🎙️ 流式TTS测试</h1>
            <p>基于火山引擎的实时语音合成服务</p>
        </div>

        <div class="content">
            <!-- 连接状态 -->
            <div class="section">
                <div id="connectionStatus" class="status status-info">
                    🔌 准备连接到服务器...
                </div>
            </div>

            <!-- 输入文本 -->
            <div class="section">
                <div class="section-title">输入文本</div>
                <div class="input-group">
                    <textarea id="textInput" rows="5" placeholder="请输入要合成的文本...">你好，欢迎使用火山引擎实时语音合成服务。这是一个流式TTS测试示例。</textarea>
                </div>
            </div>

            <!-- 参数配置 -->
            <div class="section">
                <div class="section-title">参数配置</div>
                <div class="params-grid">
                    <div class="input-group">
                        <label>语音类型</label>
                        <select id="voiceType">
                            <option value="BV009_streaming">通用女声</option>
                            <option value="BV001_streaming">通用男声</option>
                            <option value="BV002_streaming">通用女声2</option>
                            <option value="BV005_streaming">通用男声2</option>
                        </select>
                    </div>
                    <div class="input-group">
                        <label>编码格式</label>
                        <select id="encoding">
                            <option value="mp3">MP3</option>
                            <option value="wav">WAV</option>
                            <option value="ogg">OGG</option>
                        </select>
                    </div>
                    <div class="input-group">
                        <label>语速 (0.5-2.0)</label>
                        <input type="number" id="speedRatio" min="0.5" max="2.0" step="0.1" value="1.0">
                    </div>
                    <div class="input-group">
                        <label>音量 (0.5-2.0)</label>
                        <input type="number" id="volumeRatio" min="0.5" max="2.0" step="0.1" value="1.0">
                    </div>
                    <div class="input-group">
                        <label>音调 (0.5-2.0)</label>
                        <input type="number" id="pitchRatio" min="0.5" max="2.0" step="0.1" value="1.0">
                    </div>
                </div>
            </div>

            <!-- 控制按钮 -->
            <div class="button-group">
                <button id="startBtn" class="btn-primary" onclick="startTTS()">
                    🎙️ 开始合成
                </button>
                <button id="stopBtn" class="btn-danger" onclick="stopTTS()" disabled>
                    🛑 停止合成
                </button>
                <button id="playBtn" class="btn-secondary" onclick="playAudio()" disabled>
                    ▶️ 播放
                </button>
                <button id="downloadBtn" class="btn-secondary" onclick="downloadAudio()" disabled>
                    💾 下载
                </button>
            </div>

            <!-- 合成状态 -->
            <div class="section" id="synthesisStatus" style="display: none;">
                <div class="section-title">合成状态</div>
                <div id="statusMessage" class="status status-info">
                    ⏳ 准备中...
                </div>
                <div class="progress-bar">
                    <div id="progressFill" class="progress-fill"></div>
                </div>
                <div class="stats">
                    <div class="stat-card">
                        <div id="chunkCount" class="stat-value">0</div>
                        <div class="stat-label">音频块数</div>
                    </div>
                    <div class="stat-card">
                        <div id="totalSize" class="stat-value">0 KB</div>
                        <div class="stat-label">数据大小</div>
                    </div>
                    <div class="stat-card">
                        <div id="duration" class="stat-value">0s</div>
                        <div class="stat-label">合成时长</div>
                    </div>
                </div>
            </div>

            <!-- 音频播放器 -->
            <div class="section" id="audioSection" style="display: none;">
                <div class="section-title">音频播放</div>
                <audio id="audioPlayer" controls></audio>
            </div>

            <!-- 日志 -->
            <div class="section">
                <div class="section-title">日志</div>
                <div class="log-container" id="logContainer"></div>
            </div>
        </div>
    </div>

    <script>
        let socket = null;
        let audioChunks = [];
        let startTime = 0;
        let chunkCount = 0;
        let totalSize = 0;
        let encoding = 'mp3';

        // 连接到服务器
        function connectToServer() {
            socket = io('http://localhost:18080', {
                transports: ['websocket'],
                reconnection: true
            });

            socket.on('connect', () => {
                updateConnectionStatus('success', '✅ 已连接到服务器');
                addLog('info', '已连接到服务器');
            });

            socket.on('disconnect', () => {
                updateConnectionStatus('error', '❌ 与服务器断开连接');
                addLog('error', '与服务器断开连接');
            });

            // TTS事件监听
            socket.on('tts_started', (data) => {
                addLog('success', `TTS合成已启动: ${data.voice_type}`);
                showSynthesisStatus();
                updateStatus('info', '⏳ 正在合成中...');
                startTime = Date.now();
            });

            socket.on('tts_audio_chunk', (data) => {
                // 接收音频块
                const audioData = base64ToArrayBuffer(data.audio_data);
                audioChunks.push(audioData);
                
                chunkCount = data.chunk_index;
                totalSize = data.total_size;
                
                // 更新统计
                document.getElementById('chunkCount').textContent = chunkCount;
                document.getElementById('totalSize').textContent = (totalSize / 1024).toFixed(2) + ' KB';
                
                const elapsed = (Date.now() - startTime) / 1000;
                document.getElementById('duration').textContent = elapsed.toFixed(1) + 's';
                
                // 更新进度（假设最多100块）
                const progress = Math.min((chunkCount / 100) * 100, 100);
                document.getElementById('progressFill').style.width = progress + '%';
                
                if (data.is_last) {
                    addLog('success', `最后一个音频块: ${chunkCount} 块, ${(totalSize/1024).toFixed(2)} KB`);
                }
            });

            socket.on('tts_complete', (data) => {
                const elapsed = (Date.now() - startTime) / 1000;
                updateStatus('success', `✅ 合成完成！总块数: ${data.total_chunks}, 总大小: ${(data.total_size/1024).toFixed(2)} KB, 耗时: ${elapsed.toFixed(2)}秒`);
                addLog('success', `合成完成: ${data.total_chunks} 块, ${(data.total_size/1024).toFixed(2)} KB`);
                
                document.getElementById('progressFill').style.width = '100%';
                
                // 合并音频并播放
                mergeAndPlayAudio();
                
                // 启用按钮
                document.getElementById('startBtn').disabled = false;
                document.getElementById('stopBtn').disabled = true;
                document.getElementById('playBtn').disabled = false;
                document.getElementById('downloadBtn').disabled = false;
            });

            socket.on('tts_error', (data) => {
                updateStatus('error', `❌ 错误: ${data.message}`);
                addLog('error', `错误: ${data.message}`);
                
                // 恢复按钮状态
                document.getElementById('startBtn').disabled = false;
                document.getElementById('stopBtn').disabled = true;
            });

            socket.on('tts_stopped', (data) => {
                updateStatus('warning', '⚠️ 合成已停止');
                addLog('warning', '合成已停止');
                
                document.getElementById('startBtn').disabled = false;
                document.getElementById('stopBtn').disabled = true;
            });
        }

        // 开始TTS合成
        function startTTS() {
            const text = document.getElementById('textInput').value.trim();
            if (!text) {
                alert('请输入要合成的文本！');
                return;
            }

            // 重置状态
            audioChunks = [];
            chunkCount = 0;
            totalSize = 0;
            
            // 获取参数
            const voiceType = document.getElementById('voiceType').value;
            encoding = document.getElementById('encoding').value;
            const speedRatio = parseFloat(document.getElementById('speedRatio').value);
            const volumeRatio = parseFloat(document.getElementById('volumeRatio').value);
            const pitchRatio = parseFloat(document.getElementById('pitchRatio').value);

            // 发送请求
            socket.emit('start_streaming_tts', {
                text: text,
                voice_type: voiceType,
                encoding: encoding,
                speed_ratio: speedRatio,
                volume_ratio: volumeRatio,
                pitch_ratio: pitchRatio
            });

            // 更新按钮状态
            document.getElementById('startBtn').disabled = true;
            document.getElementById('stopBtn').disabled = false;
            document.getElementById('playBtn').disabled = true;
            document.getElementById('downloadBtn').disabled = true;

            addLog('info', `开始合成: ${text.substring(0, 30)}...`);
        }

        // 停止TTS合成
        function stopTTS() {
            socket.emit('stop_streaming_tts');
            addLog('warning', '请求停止合成');
        }

        // 合并并播放音频
        function mergeAndPlayAudio() {
            if (audioChunks.length === 0) return;

            // 合并所有音频块
            const totalLength = audioChunks.reduce((acc, chunk) => acc + chunk.byteLength, 0);
            const mergedArray = new Uint8Array(totalLength);
            let offset = 0;
            
            for (const chunk of audioChunks) {
                mergedArray.set(new Uint8Array(chunk), offset);
                offset += chunk.byteLength;
            }

            // 创建Blob
            const mimeType = encoding === 'mp3' ? 'audio/mpeg' : 
                           encoding === 'wav' ? 'audio/wav' : 
                           'audio/ogg';
            const blob = new Blob([mergedArray], { type: mimeType });
            const url = URL.createObjectURL(blob);

            // 设置音频播放器
            const audioPlayer = document.getElementById('audioPlayer');
            audioPlayer.src = url;
            
            // 显示音频播放器
            document.getElementById('audioSection').style.display = 'block';
            
            addLog('success', '音频已准备播放');
        }

        // 播放音频
        function playAudio() {
            const audioPlayer = document.getElementById('audioPlayer');
            audioPlayer.play();
            addLog('info', '播放音频');
        }

        // 下载音频
        function downloadAudio() {
            if (audioChunks.length === 0) {
                alert('没有可下载的音频！');
                return;
            }

            const audioPlayer = document.getElementById('audioPlayer');
            const url = audioPlayer.src;
            
            const a = document.createElement('a');
            a.href = url;
            a.download = `tts_output_${Date.now()}.${encoding}`;
            a.click();
            
            addLog('success', '下载音频');
        }

        // Base64转ArrayBuffer
        function base64ToArrayBuffer(base64) {
            const binaryString = atob(base64);
            const bytes = new Uint8Array(binaryString.length);
            for (let i = 0; i < binaryString.length; i++) {
                bytes[i] = binaryString.charCodeAt(i);
            }
            return bytes.buffer;
        }

        // 更新连接状态
        function updateConnectionStatus(type, message) {
            const statusEl = document.getElementById('connectionStatus');
            statusEl.className = `status status-${type}`;
            statusEl.textContent = message;
        }

        // 显示合成状态区域
        function showSynthesisStatus() {
            document.getElementById('synthesisStatus').style.display = 'block';
            document.getElementById('chunkCount').textContent = '0';
            document.getElementById('totalSize').textContent = '0 KB';
            document.getElementById('duration').textContent = '0s';
            document.getElementById('progressFill').style.width = '0%';
        }

        // 更新状态消息
        function updateStatus(type, message) {
            const statusEl = document.getElementById('statusMessage');
            statusEl.className = `status status-${type}`;
            statusEl.textContent = message;
        }

        // 添加日志
        function addLog(type, message) {
            const logContainer = document.getElementById('logContainer');
            const time = new Date().toLocaleTimeString();
            
            const logEntry = document.createElement('div');
            logEntry.className = 'log-entry';
            logEntry.innerHTML = `<span class="log-time">[${time}]</span>${message}`;
            
            logContainer.appendChild(logEntry);
            logContainer.scrollTop = logContainer.scrollHeight;
        }

        // 页面加载时连接
        window.onload = () => {
            connectToServer();
        };
    </script>
</body>
</html>

